import os
import sys
import Image
import math


a_f = -0.1
v_f = 0.5
a_l = -0.1
v_l = 0.05
a_t = -0.2
v_t = 20
linking_strength = 0.3
convolution_kernel_radius = 1
uses_fast_linking = False

class Neuron:
    def __init__(self):
        self.x = 0
        self.y = 0

        self.Q = []
        self.F = 0
        self.L = 0
        self.Y = 0
        self.U = 0
        self.T = 0.5

    def feeding(self, S):
        self.F = math.exp(a_f) * self.F + S + v_f * sum(self.Q)

    def linking(self):
        self.L = math.exp(a_l) * self.L + v_l * sum(self.Q)

    def internal_activity(self):
        self.U = self.F * (1 + linking_strength * self.L)

    def output_impulse(self):
        if (self.U > self.T):
            self.Y = 1
        else:
            self.Y = 0

    def threshold(self):
        self.T =  math.exp(a_t) * self.T + v_t * self.Y


class NeuralNetwork:
    def __init__(self):
        self.neurons = []
        self.convolution_kernel_radius = 0
        self.size = ()
        self.uses_fast_linking = False


    def init(self, im, convolution_kernel_radius, uses_fast_linking):
        self.convolution_kernel_radius = convolution_kernel_radius
        self.size = im.size
        self.uses_fast_linking = uses_fast_linking
        for x in range(im.size[0]):
            self.neurons.append([]);
            for y in range(im.size[1]):
                neuron = Neuron()
                neuron.x = x
                neuron.y = y
                self.neurons[x].append(neuron)

    def get_neighbours(self, neuron):
        neighbours = []
        top = neuron.y - self.convolution_kernel_radius
        left = neuron.x - self.convolution_kernel_radius
        bottom = neuron.y + self.convolution_kernel_radius
        right = neuron.x + self.convolution_kernel_radius

        # check bounds
        def check_bounds(n, axis):
            if (n < 0): n = 0
            if (axis == 'x'):
                if (n >= self.size[0]): n = self.size[0] - 1
            elif (axis == 'y'):
                if (n >= self.size[1]): n = self.size[1] - 1
            return n
        top = check_bounds(top, 'y')
        left = check_bounds(left, 'x')
        bottom = check_bounds(bottom, 'y')
        right = check_bounds(right, 'x')

        # add neighbour coordinates to list
        for x in range(left, right + 1):
            for y in range(top, bottom + 1):
                if (x != neuron.x or y != neuron.y):
                    neighbours.append((x, y))
        return neighbours

    def run(self, input, iteration):

        # neighbouring neurons input assigning function
        def assign_neighbouring_inputs(neuron):
            neighbour_inputs = []
            for neighbour in [self.neurons[xy[0]][xy[1]] for xy in self.get_neighbours(neuron)]:
                neighbour_inputs.append(neighbour.Y)
            neuron.Q = neighbour_inputs

        # compute internal activities of all neurons
        for neuron_row in self.neurons:
            for neuron in neuron_row:
                assign_neighbouring_inputs(neuron)          # get neighbouring neurons impulses
                neuron.feeding(input[neuron.x][neuron.y])   # compute feeding compartment value
                neuron.linking()                            # compute linking compartment value
                neuron.internal_activity()                  # compute iternal activity
        Y = []
        # compute output impulses of all neurons
        for neuron_row in self.neurons:
            Y.append([])
            for neuron in neuron_row:
                neuron.output_impulse()                     # compare iternal activity to dynamic threshold
                Y[neuron.x].append(neuron.Y)


        # fast linking
        if (self.uses_fast_linking):
            staticY = [[False for b in a[:]] for a in Y]
            cnt = 0
            while (cnt != len(staticY) * len (staticY[0])):
                for neuron_row in self.neurons:
                    for neuron in neuron_row:
                        if not(staticY[neuron.x][neuron.y]):
                            assign_neighbouring_inputs(neuron)
                            #neuron.L = v_l * sum(neuron.Q)
                            neuron.internal_activity()
                            neuron.U = neuron.F * (1 + linking_strength * v_l * sum(neuron.Q))

                for neuron_row in self.neurons:
                    for neuron in neuron_row:
                        if not(staticY[neuron.x][neuron.y]):
                            prevY = neuron.Y
                            neuron.output_impulse()
                            if (prevY == neuron.Y):
                                staticY[neuron.x][neuron.y] = True
                                cnt +=1


        # compute dynamic thresholds of all neurons
        for neuron_row in self.neurons:
            for neuron in neuron_row:
                neuron.threshold()                          # compute dynamic threshold


        # TODO: remove this output
        #print "[%d]" % iteration, self.neurons[100][100].U, self.neurons[100][100].F, self.neurons[100][100].L, self.neurons[100][100].T
        return sum([sum(y) for y in Y])


def main():
    # load image for processing
    im = Image.open(sys.argv[1])

    # init network with image
    nn = NeuralNetwork()
    nn.init(im, convolution_kernel_radius, uses_fast_linking)

    # TESTS
    pix = im.load()
    input = []
    for x in range(im.size[0]):
        input.append([])
        for y in range(im.size[1]):
            # TODO: right now middle value of RGB is taken
            value = (255 - (pix[x, y][0] + pix[x, y][1] + pix[x, y][2]) / 3) / 255.0
            input[x].append(value)   # dark pixel is positive impulse
            #input[x].append(pix[x, y][0] / 255.0)           # bright pixel is positive impulse



    def get_pulse_output(neural_network, iteration):
        for neuron_row in neural_network.neurons:
            for neuron in neuron_row:
                pulse_val = (1 - neuron.Y) * 255 # positive output is a black pixel
                pix[neuron.x, neuron.y] = (pulse_val, pulse_val, pulse_val)
        im.save("{0}/test{1}.jpg".format(sys.argv[3], iteration))

    time_signals = []
    for i in range(int(sys.argv[2])):
        G = nn.run(input, i)
        get_pulse_output(nn, i)
        time_signals.append(G)



    # print time signal this way for easy parsing
    str = ""
    for x in time_signals:
        str += "{0} ".format(float(x)/(im.size[0]*im.size[1])*100)

    print str


if __name__ == "__main__":
    main()
